/* ---------------------------- dom/element.js ------------------------------ */
VED.dom.element = VED.dom.node.extend({
   initialize: function( element, ownerDocument ) {
      if ( typeof element == 'string' )
         element = ( ownerDocument ? ownerDocument.$ : document ).createElement( element );

      // Call the base constructor (we must not call VED.dom.node).
      this.parent.parent.call( this, element );
   }
});

VED.dom.element.get = function( element ) {
   var el = typeof element == 'string' ? document.getElementById( element ) || document.getElementsByName( element )[ 0 ] : element;

   return el && ( el.$ ? el : new VED.dom.element( el ) );
};

VED.dom.element.createFromHtml = function( html, ownerDocument ) {
   var temp = new VED.dom.element( 'div', ownerDocument );
   temp.setHtml( html );

   // When returning the node, remove it from its parent to detach it.
   return temp.getFirst().remove();
};

VED.dom.element.setMarker = function( database, element, name, value ) {
   var id = element.getCustomData( 'list_marker_id' ) || ( element.setCustomData( 'list_marker_id', VED.tools.getNextNumber() ).getCustomData( 'list_marker_id' ) ),
      markerNames = element.getCustomData( 'list_marker_names' ) || ( element.setCustomData( 'list_marker_names', {} ).getCustomData( 'list_marker_names' ) );
   database[ id ] = element;
   markerNames[ name ] = 1;

   return element.setCustomData( name, value );
};

VED.dom.element.clearAllMarkers = function( database ) {
   for ( var i in database )
      VED.dom.element.clearMarkers( database, database[ i ], 1 );
};

VED.dom.element.clearMarkers = function( database, element, removeFromDatabase ) {
   var names = element.getCustomData( 'list_marker_names' ),
      id = element.getCustomData( 'list_marker_id' );
   for ( var i in names )
      element.removeCustomData( i );
   element.removeCustomData( 'list_marker_names' );
   if ( removeFromDatabase ) {
      element.removeCustomData( 'list_marker_id' );
      delete database[ id ];
   }
};
( function() {

VED.dom.element.implement({

   type: VED.NODE_ELEMENT,

   addClass: function( className ) {
      var c = this.$.className;
      if ( c ) {
         var regex = new RegExp( '(?:^|\\s)' + className + '(?:\\s|$)', '' );
         if ( !regex.test( c ) )
            c += ' ' + className;
      }
      this.$.className = c || className;
   },

   removeClass: function( className ) {
      var c = this.getAttribute( 'class' );
      if ( c ) {
         var regex = new RegExp( '(?:^|\\s+)' + className + '(?=\\s|$)', 'i' );
         if ( regex.test( c ) ) {
            c = c.replace( regex, '' ).replace( /^\s+/, '' );

            if ( c )
               this.setAttribute( 'class', c );
            else
               this.removeAttribute( 'class' );
         }
      }

      return this;
   },

   hasClass: function( className ) {
      var regex = new RegExp( '(?:^|\\s+)' + className + '(?=\\s|$)', '' );
      return regex.test( this.getAttribute( 'class' ) );
   },

   append: function( node, toStart ) {
      if ( typeof node == 'string' )
         node = this.getDocument().createElement( node );

      if ( toStart )
         this.$.insertBefore( node.$, this.$.firstChild );
      else
         this.$.appendChild( node.$ );

      return node;
   },

   appendHtml: function( html ) {
      if ( !this.$.childNodes.length )
         this.setHtml( html );
      else {
         var temp = new VED.dom.element( 'div', this.getDocument() );
         temp.setHtml( html );
         temp.moveChildren( this );
      }
   },

   appendText: function( text ) {
      if ( this.$.text != undefined )
         this.$.text += text;
      else
         this.append( new VED.dom.text( text ) );
   },

   appendBogus: function() {
      var lastChild = this.getLast();

      // Ignore empty/spaces text.
      while ( lastChild && lastChild.type == VED.NODE_TEXT && !lastChild.getText().rtrim() )
         lastChild = lastChild.getPrevious();
      if ( !lastChild || !lastChild.is || !lastChild.is( 'br' ) ) {
         var bogus = VED.env.opera ? this.getDocument().createText( '' ) : this.getDocument().createElement( 'br' );

         VED.env.gecko && bogus.setAttribute( 'type', '_moz' );

         this.append( bogus );
      }
   },

   breakParent: function( parent ) {
      var range = new VED.dom.range( this.getDocument() );

      // We'll be extracting part of this element, so let's use our
      // range to get the correct piece.
      range.setStartAfter( this );
      range.setEndAfter( parent );

      // Extract it.
      var docFrag = range.extractContents();

      // Move the element outside the broken element.
      range.insertNode( this.remove() );

      // Re-insert the extracted piece after the element.
      docFrag.insertAfterNode( this );
   },

   contains: VED.env.ie || VED.env.webkit ?
      function( node ) {
         var $ = this.$;

         return node.type != VED.NODE_ELEMENT ? $.contains( node.getParent().$ ) : $ != node.$ && $.contains( node.$ );
      } : function( node ) {
         return !!( this.$.compareDocumentPosition( node.$ ) & 16 );
      },

   focus: (function() {
      function exec() {
         // IE throws error if the element is not visible.
         try {
            this.$.focus();
         } catch ( e ) {}
      }

      return function( defer ) {
         if ( defer )
            VED.tools.setTimeout( exec, 100, this );
         else
            exec.call( this );
      };
   })(),

   getHtml: function() {
      var retval = this.$.innerHTML;
      // Strip <?xml:namespace> tags in IE. (#3341).
      return VED.env.ie ? retval.replace( /<\?[^>]*>/g, '' ) : retval;
   },

   getOuterHtml: function() {
      if ( this.$.outerHTML ) {
         // IE includes the <?xml:namespace> tag in the outerHTML of
         // namespaced element. So, we must strip it here. (#3341)
         return this.$.outerHTML.replace( /<\?[^>]*>/, '' );
      }

      var tmpDiv = this.$.ownerDocument.createElement( 'div' );
      tmpDiv.appendChild( this.$.cloneNode( true ) );
      return tmpDiv.innerHTML;
   },

   getClientRect: function() {
      // http://help.dottoro.com/ljvmcrrn.php
      var rect = VED.tools.extend( {}, this.$.getBoundingClientRect() );

      !rect.width && ( rect.width = rect.right - rect.left );
      !rect.height && ( rect.height = rect.bottom - rect.top );

      return rect;
   },

   setHtml: (function() {
      var standard = function( html ) {
         return ( this.$.innerHTML = html );
      };

      if ( VED.env.ie && VED.env.version < 9 ) {
         // old IEs throws error on HTML manipulation (through the "innerHTML" property)
         // on the element which resides in an DTD invalid position,  e.g. <span><div></div></span>
         // fortunately it can be worked around with DOM manipulation.
         return function( html ) {
            try { return standard.call( this, html ); }
            catch ( e ) {
               this.$.innerHTML = '';

               var temp = new VED.dom.element( 'body', this.getDocument() );
               temp.$.innerHTML = html;

               var children = temp.getChildren();
               while( children.count() )
                  this.append( children.getItem( 0 ) );

               return html;
            }
         };
      } else
         return standard;
   })(),

   setText: function( text ) {
      VED.dom.element.prototype.setText = ( this.$.innerText != undefined ) ?
         function( text ) {
            return this.$.innerText = text;
         } : function( text ) {
            return this.$.textContent = text;
         };

      return this.setText( text );
   },

   getAttribute: (function() {
      var standard = function( name ) {
            return this.$.getAttribute( name, 2 );
         };

      if ( VED.env.ie && ( VED.env.ie7Compat || VED.env.ie6Compat ) ) {
         return function( name ) {
            switch ( name ) {
               case 'class':
                  name = 'className';
                  break;

               case 'http-equiv':
                  name = 'httpEquiv';
                  break;

               case 'name':
                  return this.$.name;

               case 'tabindex':
                  var tabIndex = standard.call( this, name );

                  // IE returns tabIndex=0 by default for all
                  // elements. For those elements,
                  // getAtrribute( 'tabindex', 2 ) returns 32768
                  // instead. So, we must make this check to give a
                  // uniform result among all browsers.
                  if ( tabIndex !== 0 && this.$.tabIndex === 0 )
                     tabIndex = null;

                  return tabIndex;
                  break;

               case 'checked':
                  {
                     var attr = this.$.attributes.getNamedItem( name ),
                        attrValue = attr.specified ? attr.nodeValue // For value given by parser.
                        : this.$.checked; // For value created via DOM interface.

                     return attrValue ? 'checked' : null;
                  }

               case 'hspace':
               case 'value':
                  return this.$[ name ];

               case 'style':
                  // IE does not return inline styles via getAttribute(). See #2947.
                  return this.$.style.cssText;

               case 'contenteditable':
               case 'contentEditable':
                  return this.$.attributes.getNamedItem( 'contentEditable' ).specified ? this.$.getAttribute( 'contentEditable' ) : null;
            }

            return standard.call( this, name );
         };
      } else
         return standard;
   })(),

   getChildren: function() {
      return new VED.dom.nodeList( this.$.childNodes );
   },

   getComputedStyle: VED.env.ie ?
      function( propertyName ) {
         return this.$.currentStyle[ VED.tools.cssStyleToDomStyle( propertyName ) ];
      } : function( propertyName ) {
         var style = this.getWindow().$.getComputedStyle( this.$, null );
         // Firefox may return null if we call the above on a hidden iframe. (#9117)
         return style ? style.getPropertyValue( propertyName ) : '';
      },

   getDtd: function() {
      var dtd = VED.dtd[ this.getName() ];

      this.getDtd = function() {
         return dtd;
      };

      return dtd;
   },

   getElementsByTag: VED.dom.document.prototype.getElementsByTag,

   getTabIndex: VED.env.ie ?
      function() {
         var tabIndex = this.$.tabIndex;

         // IE returns tabIndex=0 by default for all elements. In
         // those cases we must check that the element really has
         // the tabindex attribute set to zero, or it is one of
         // those element that should have zero by default.
         if ( tabIndex === 0 && !VED.dtd.$tabIndex[ this.getName() ] && parseInt( this.getAttribute( 'tabindex' ), 10 ) !== 0 )
            tabIndex = -1;

         return tabIndex;
      } : VED.env.webkit ?
      function() {
         var tabIndex = this.$.tabIndex;

         // Safari returns "undefined" for elements that should not
         // have tabindex (like a div). So, we must try to get it
         // from the attribute.
         // https://bugs.webkit.org/show_bug.cgi?id=20596
         if ( tabIndex == undefined ) {
            tabIndex = parseInt( this.getAttribute( 'tabindex' ), 10 );

            // If the element don't have the tabindex attribute,
            // then we should return -1.
            if ( isNaN( tabIndex ) )
               tabIndex = -1;
         }

         return tabIndex;
      } : function() {
         return this.$.tabIndex;
      },

   getText: function() {
      return this.$.textContent || this.$.innerText || '';
   },

   getWindow: function() {
      return this.getDocument().getWindow();
   },

   getId: function() {
      return this.$.id || null;
   },

   getNameAtt: function() {
      return this.$.name || null;
   },

   getName: function() {
      // Cache the lowercased name inside a closure.
      var nodeName = this.$.nodeName.toLowerCase();

      if ( VED.env.ie && !( document.documentMode > 8 ) ) {
         var scopeName = this.$.scopeName;
         if ( scopeName != 'HTML' )
            nodeName = scopeName.toLowerCase() + ':' + nodeName;
      }

      return ( this.getName = function() {
         return nodeName;
      })();
   },

   getValue: function() {
      return this.$.value;
   },

   getFirst: function( evaluator ) {
      var first = this.$.firstChild,
         retval = first && new VED.dom.node( first );
      if ( retval && evaluator && !evaluator( retval ) )
         retval = retval.getNext( evaluator );

      return retval;
   },

   getLast: function( evaluator ) {
      var last = this.$.lastChild,
         retval = last && new VED.dom.node( last );
      if ( retval && evaluator && !evaluator( retval ) )
         retval = retval.getPrevious( evaluator );

      return retval;
   },

   getStyle: function( name ) {
      return this.$.style[ VED.tools.cssStyleToDomStyle( name ) ];
   },

   is: function() {
      var name = this.getName();

      // Check against the specified DTD liternal.
      if ( typeof arguments[ 0 ] == 'object' )
         return !!arguments[ 0 ][ name ];

      // Check for tag names
      for ( var i = 0; i < arguments.length; i++ ) {
         if ( arguments[ i ] == name )
            return true;
      }
      return false;
   },

   isEditable: function( textCursor ) {
      var name = this.getName();

      if ( this.isReadOnly() || this.getComputedStyle( 'display' ) == 'none' ||
             this.getComputedStyle( 'visibility' ) == 'hidden' ||
             VED.dtd.$nonEditable[ name ] ||
             VED.dtd.$empty[ name ] ||
             ( this.is( 'a' ) &&
                ( this.data( 'ved-saved-name' ) || this.hasAttribute( 'name' ) ) &&
                !this.getChildCount()
             ) )
      {
         return false;
      }

      if ( textCursor !== false ) {
         // Get the element DTD (defaults to span for unknown elements).
         var dtd = VED.dtd[ name ] || VED.dtd.span;
         // In the DTD # == text node.
         return !!( dtd && dtd[ '#' ] );
      }

      return true;
   },

   isIdentical: function( otherElement ) {
      // do shallow clones, but with IDs
      var thisEl = this.clone( 0 ),
         otherEl = otherElement.clone( 0 );

      // Remove distractions.
      thisEl.removeAttributes( [ '_moz_dirty', 'data-ved-expando', 'data-ved-saved-href', 'data-ved-saved-name' ] );
      otherEl.removeAttributes( [ '_moz_dirty', 'data-ved-expando', 'data-ved-saved-href', 'data-ved-saved-name' ] );

      // Native comparison available.
      if ( thisEl.isEqualNode ) {
         // Styles order matters.
         thisEl.$.style.cssText = VED.tools.normalizeCssText( thisEl.$.style.cssText );
         otherEl.$.style.cssText = VED.tools.normalizeCssText( otherEl.$.style.cssText );
         return thisEl.$.isEqualNode( otherEl.$ );
      } else {
         thisEl = thisEl.getOuterHtml();
         otherEl = otherEl.getOuterHtml();

         // Fix tiny difference between link href in older IEs.
         if ( VED.env.ie && VED.env.version < 9 && this.is( 'a' ) ) {
            var parent = this.getParent();
            if ( parent.type == VED.NODE_ELEMENT ) {
               var el = parent.clone();
               el.setHtml( thisEl ), thisEl = el.getHtml();
               el.setHtml( otherEl ), otherEl = el.getHtml();
            }
         }

         return thisEl == otherEl;
      }
   },

   isVisible: function() {
      var isVisible = ( this.$.offsetHeight || this.$.offsetWidth ) && this.getComputedStyle( 'visibility' ) != 'hidden',
         elementWindow, elementWindowFrame;

      // Webkit and Opera report non-zero offsetHeight despite that
      // element is inside an invisible iframe. (#4542)
      if ( isVisible && ( VED.env.webkit || VED.env.opera ) ) {
         elementWindow = this.getWindow();

         if ( !elementWindow.equals( VED.document.getWindow() ) && ( elementWindowFrame = elementWindow.$.frameElement ) ) {
            isVisible = new VED.dom.element( elementWindowFrame ).isVisible();
         }
      }

      return !!isVisible;
   },

   isEmptyInlineRemoveable: function() {
      if ( !VED.dtd.$removeEmpty[ this.getName() ] )
         return false;

      var children = this.getChildren();
      for ( var i = 0, count = children.count(); i < count; i++ ) {
         var child = children.getItem( i );

         if ( child.type == VED.NODE_ELEMENT && child.data( 'ved-bookmark' ) )
            continue;

         if ( child.type == VED.NODE_ELEMENT && !child.isEmptyInlineRemoveable() || child.type == VED.NODE_TEXT && child.getText().trim() ) {
            return false;
         }
      }
      return true;
   },

   hasAttributes: VED.env.ie && ( VED.env.ie7Compat || VED.env.ie6Compat ) ?
      function() {
         var attributes = this.$.attributes;

         for ( var i = 0; i < attributes.length; i++ ) {
            var attribute = attributes[ i ];

            switch ( attribute.nodeName ) {
               case 'class':
                  // IE has a strange bug. If calling removeAttribute('className'),
                  // the attributes collection will still contain the "class"
                  // attribute, which will be marked as "specified", even if the
                  // outerHTML of the element is not displaying the class attribute.
                  // Note : I was not able to reproduce it outside the editor,
                  // but I've faced it while working on the TC of #1391.
                  if ( this.getAttribute( 'class' ) )
                     return true;

                  // Attributes to be ignored.
               case 'data-ved-expando':
                  continue;

                  /*jsl:fallthru*/

               default:
                  if ( attribute.specified )
                     return true;
            }
         }

         return false;
      } : function() {
         var attrs = this.$.attributes,
            attrsNum = attrs.length;

         // The _moz_dirty attribute might get into the element after pasting (#5455)
         var execludeAttrs = { 'data-ved-expando':1,_moz_dirty:1 };

         return attrsNum > 0 && ( attrsNum > 2 || !execludeAttrs[ attrs[ 0 ].nodeName ] || ( attrsNum == 2 && !execludeAttrs[ attrs[ 1 ].nodeName ] ) );
      },

   hasAttribute: (function() {
      function standard( name ) {
         var $attr = this.$.attributes.getNamedItem( name );
         return !!( $attr && $attr.specified );
      }

      return ( VED.env.ie && VED.env.version < 8 ) ?
      function( name ) {
         // On IE < 8 the name attribute cannot be retrieved
         // right after the element creation and setting the
         // name with setAttribute.
         if ( name == 'name' )
            return !!this.$.name;

         return standard.call( this, name );
      } : standard;
   })(),

   hide: function() {
      this.setStyle( 'display', 'none' );
   },

   moveChildren: function( target, toStart ) {
      var $ = this.$;
      target = target.$;

      if ( $ == target )
         return;

      var child;

      if ( toStart ) {
         while ( ( child = $.lastChild ) )
            target.insertBefore( $.removeChild( child ), target.firstChild );
      } else {
         while ( ( child = $.firstChild ) )
            target.appendChild( $.removeChild( child ) );
      }
   },

   mergeSiblings: (function() {
      function mergeElements( element, sibling, isNext ) {
         if ( sibling && sibling.type == VED.NODE_ELEMENT ) {
            // Jumping over bookmark nodes and empty inline elements, e.g. <b><i></i></b>,
            // queuing them to be moved later. (#5567)
            var pendingNodes = [];

            while ( sibling.data( 'ved-bookmark' ) || sibling.isEmptyInlineRemoveable() ) {
               pendingNodes.push( sibling );
               sibling = isNext ? sibling.getNext() : sibling.getPrevious();
               if ( !sibling || sibling.type != VED.NODE_ELEMENT )
                  return;
            }

            if ( element.isIdentical( sibling ) ) {
               // Save the last child to be checked too, to merge things like
               // <b><i></i></b><b><i></i></b> => <b><i></i></b>
               var innerSibling = isNext ? element.getLast() : element.getFirst();

               // Move pending nodes first into the target element.
               while ( pendingNodes.length )
                  pendingNodes.shift().move( element, !isNext );

               sibling.moveChildren( element, !isNext );
               sibling.remove();

               // Now check the last inner child (see two comments above).
               if ( innerSibling && innerSibling.type == VED.NODE_ELEMENT )
                  innerSibling.mergeSiblings();
            }
         }
      }

      return function( inlineOnly ) {
         if ( !( inlineOnly === false || VED.dtd.$removeEmpty[ this.getName() ] || this.is( 'a' ) ) ) // Merge empty links and anchors also. (#5567)
         {
            return;
         }

         mergeElements( this, this.getNext(), true );
         mergeElements( this, this.getPrevious() );
      };
   })(),

   show: function() {
      this.setStyles({
         display: '',
         visibility: ''
      });
   },

   setAttribute: (function() {
      var standard = function( name, value ) {
            this.$.setAttribute( name, value );
            return this;
         };

      if ( VED.env.ie && ( VED.env.ie7Compat || VED.env.ie6Compat ) ) {
         return function( name, value ) {
            if ( name == 'class' )
               this.$.className = value;
            else if ( name == 'style' )
               this.$.style.cssText = value;
            else if ( name == 'tabindex' ) // Case sensitive.
            this.$.tabIndex = value;
            else if ( name == 'checked' )
               this.$.checked = value;
            else if ( name == 'contenteditable' )
               standard.call( this, 'contentEditable', value );
            else
               standard.apply( this, arguments );
            return this;
         };
      } else if ( VED.env.ie8Compat && VED.env.secure ) {
         return function( name, value ) {
            // IE8 throws error when setting src attribute to non-ssl value. (#7847)
            if ( name == 'src' && value.match( /^http:\/\// ) )
               try {
               standard.apply( this, arguments );
            } catch ( e ) {} else
               standard.apply( this, arguments );
            return this;
         };
      } else
         return standard;
   })(),

   setAttributes: function( attributesPairs ) {
      for ( var name in attributesPairs )
         this.setAttribute( name, attributesPairs[ name ] );
      return this;
   },

   setValue: function( value ) {
      this.$.value = value;
      return this;
   },

   removeAttribute: (function() {
      var standard = function( name ) {
            this.$.removeAttribute( name );
         };

      if ( VED.env.ie && ( VED.env.ie7Compat || VED.env.ie6Compat ) ) {
         return function( name ) {
            if ( name == 'class' )
               name = 'className';
            else if ( name == 'tabindex' )
               name = 'tabIndex';
            else if ( name == 'contenteditable' )
               name = 'contentEditable';
            standard.call( this, name );
         };
      } else
         return standard;
   })(),

   removeAttributes: function( attributes ) {
      if ( VED.tools.isArray( attributes ) ) {
         for ( var i = 0; i < attributes.length; i++ )
            this.removeAttribute( attributes[ i ] );
      } else {
         for ( var attr in attributes )
            attributes.hasOwnProperty( attr ) && this.removeAttribute( attr );
      }
   },

   removeStyle: function( name ) {
      // Removes the specified property from the current style object.
      var $ = this.$.style;

      // "removeProperty" need to be specific on the following styles.
      if ( !$.removeProperty && ( name == 'border' || name == 'margin' || name == 'padding' ) ) {
         var names = expandedRules( name );
         for ( var i = 0 ; i < names.length ; i++ )
            this.removeStyle( names[ i ] );
         return;
      }

      $.removeProperty ? $.removeProperty( name ) : $.removeAttribute( VED.tools.cssStyleToDomStyle( name ) );

      // Eventually remove empty style attribute.
      if ( !this.$.style.cssText )
         this.removeAttribute( 'style' );
   },

   setStyle: function( name, value ) {
      this.$.style[ VED.tools.cssStyleToDomStyle( name ) ] = value;
      return this;
   },

   setStyles: function( stylesPairs ) {
      for ( var name in stylesPairs )
         this.setStyle( name, stylesPairs[ name ] );
      return this;
   },

   setOpacity: function( opacity ) {
      if ( VED.env.ie && VED.env.version < 9 ) {
         opacity = Math.round( opacity * 100 );
         this.setStyle( 'filter', opacity >= 100 ? '' : 'progid:DXImageTransform.Microsoft.Alpha(opacity=' + opacity + ')' );
      } else
         this.setStyle( 'opacity', opacity );
   },

   unselectable: function() {
      // CSS unselectable.
      this.setStyles( VED.tools.cssVendorPrefix( 'user-select', 'none' ) );

      // For IE/Opera which doesn't support for the above CSS style,
      // the unselectable="on" attribute only specifies the selection
      // process cannot start in the element itself, and it doesn't inherit.
      if ( VED.env.ie || VED.env.opera ) {
         this.setAttribute( 'unselectable', 'on' );

         var element,
            elements = this.getElementsByTag( "*" );

         for ( var i = 0, count = elements.count() ; i < count ; i++ ) {
            element = elements.getItem( i );
            element.setAttribute( 'unselectable', 'on' );
         }
      }
   },

   getPositionedAncestor: function() {
      var current = this;
      while ( current.getName() != 'html' ) {
         if ( current.getComputedStyle( 'position' ) != 'static' )
            return current;

         current = current.getParent();
      }
      return null;
   },

   getDocumentPosition: function( refDocument ) {
      var x = 0,
         y = 0,
         doc = this.getDocument(),
         body = doc.getBody(),
         quirks = doc.$.compatMode == 'BackCompat';

      if ( document.documentElement[ "getBoundingClientRect" ] ) {
         var box = this.$.getBoundingClientRect(),
            $doc = doc.$,
            $docElem = $doc.documentElement;

         var clientTop = $docElem.clientTop || body.$.clientTop || 0,
            clientLeft = $docElem.clientLeft || body.$.clientLeft || 0,
            needAdjustScrollAndBorders = true;

         // #3804: getBoundingClientRect() works differently on IE and non-IE
         // browsers, regarding scroll positions.
         //
         // On IE, the top position of the <html> element is always 0, no matter
         // how much you scrolled down.
         //
         // On other browsers, the top position of the <html> element is negative
         // scrollTop.
         if ( VED.env.ie ) {
            var inDocElem = doc.getDocumentElement().contains( this ),
               inBody = doc.getBody().contains( this );

            needAdjustScrollAndBorders = ( quirks && inBody ) || ( !quirks && inDocElem );
         }

         if ( needAdjustScrollAndBorders ) {
            x = box.left + ( !quirks && $docElem.scrollLeft || body.$.scrollLeft );
            x -= clientLeft;
            y = box.top + ( !quirks && $docElem.scrollTop || body.$.scrollTop );
            y -= clientTop;
         }
      } else {
         var current = this,
            previous = null,
            offsetParent;
         while ( current && !( current.getName() == 'body' || current.getName() == 'html' ) ) {
            x += current.$.offsetLeft - current.$.scrollLeft;
            y += current.$.offsetTop - current.$.scrollTop;

            // Opera includes clientTop|Left into offsetTop|Left.
            if ( !current.equals( this ) ) {
               x += ( current.$.clientLeft || 0 );
               y += ( current.$.clientTop || 0 );
            }

            var scrollElement = previous;
            while ( scrollElement && !scrollElement.equals( current ) ) {
               x -= scrollElement.$.scrollLeft;
               y -= scrollElement.$.scrollTop;
               scrollElement = scrollElement.getParent();
            }

            previous = current;
            current = ( offsetParent = current.$.offsetParent ) ? new VED.dom.element( offsetParent ) : null;
         }
      }

      if ( refDocument ) {
         var currentWindow = this.getWindow(),
            refWindow = refDocument.getWindow();

         if ( !currentWindow.equals( refWindow ) && currentWindow.$.frameElement ) {
            var iframePosition = ( new VED.dom.element( currentWindow.$.frameElement ) ).getDocumentPosition( refDocument );

            x += iframePosition.x;
            y += iframePosition.y;
         }
      }

      if ( !document.documentElement[ "getBoundingClientRect" ] ) {
         // In Firefox, we'll endup one pixel before the element positions,
         // so we must add it here.
         if ( VED.env.gecko && !quirks ) {
            x += this.$.clientLeft ? 1 : 0;
            y += this.$.clientTop ? 1 : 0;
         }
      }

      return { x: x, y: y };
   },

   scrollIntoView: function( alignToTop ) {
      var parent = this.getParent();
      if ( !parent )
         return;

      // Scroll the element into parent container from the inner out.
      do {
         // Check ancestors that overflows.
         var overflowed =
            parent.$.clientWidth && parent.$.clientWidth < parent.$.scrollWidth ||
            parent.$.clientHeight && parent.$.clientHeight < parent.$.scrollHeight;

         // Skip body element, which will report wrong clientHeight when containing
         // floated content. (#9523)
         if ( overflowed && !parent.is( 'body' ) )
            this.scrollIntoParent( parent, alignToTop, 1 );

         // Walk across the frame.
         if ( parent.is( 'html' ) ) {
            var win = parent.getWindow();

            // Avoid security error.
            try {
               var iframe = win.$.frameElement;
               iframe && ( parent = new VED.dom.element( iframe ) );
            } catch ( er ) {}
         }
      }
      while ( ( parent = parent.getParent() ) );
   },

   scrollIntoParent: function( parent, alignToTop, hscroll ) {
      !parent && ( parent = this.getWindow() );

      var doc = parent.getDocument();
      var isQuirks = doc.$.compatMode == 'BackCompat';

      // On window <html> is scrolled while quirks scrolls <body>.
      if ( parent instanceof VED.dom.window )
         parent = isQuirks ? doc.getBody() : doc.getDocumentElement();

      // Scroll the parent by the specified amount.
      function scrollBy( x, y ) {
         // Webkit doesn't support "scrollTop/scrollLeft"
         // on documentElement/body element.
         if ( /body|html/.test( parent.getName() ) )
            parent.getWindow().$.scrollBy( x, y );
         else {
            parent.$[ 'scrollLeft' ] += x;
            parent.$[ 'scrollTop' ] += y;
         }
      }

      // Figure out the element position relative to the specified window.
      function screenPos( element, refWin ) {
         var pos = { x: 0, y: 0 };

         if ( !( element.is( isQuirks ? 'body' : 'html' ) ) ) {
            var box = element.$.getBoundingClientRect();
            pos.x = box.left, pos.y = box.top;
         }

         var win = element.getWindow();
         if ( !win.equals( refWin ) ) {
            var outerPos = screenPos( VED.dom.element.get( win.$.frameElement ), refWin );
            pos.x += outerPos.x, pos.y += outerPos.y;
         }

         return pos;
      }

      // calculated margin size.
      function margin( element, side ) {
         return parseInt( element.getComputedStyle( 'margin-' + side ) || 0, 10 ) || 0;
      }

      var win = parent.getWindow();

      var thisPos = screenPos( this, win ),
         parentPos = screenPos( parent, win ),
         eh = this.$.offsetHeight,
         ew = this.$.offsetWidth,
         ch = parent.$.clientHeight,
         cw = parent.$.clientWidth,
         lt, br;

      // Left-top margins.
      lt = {
         x: thisPos.x - margin( this, 'left' ) - parentPos.x || 0,
         y: thisPos.y - margin( this, 'top' ) - parentPos.y || 0
      };

      // Bottom-right margins.
      br = {
         x: thisPos.x + ew + margin( this, 'right' ) - ( ( parentPos.x ) + cw ) || 0,
         y: thisPos.y + eh + margin( this, 'bottom' ) - ( ( parentPos.y ) + ch ) || 0
      };

      // 1. Do the specified alignment as much as possible;
      // 2. Otherwise be smart to scroll only the minimum amount;
      // 3. Never cut at the top;
      // 4. DO NOT scroll when already visible.
      if ( lt.y < 0 || br.y > 0 ) {
         scrollBy( 0, alignToTop === true ? lt.y : alignToTop === false ? br.y : lt.y < 0 ? lt.y : br.y );
      }

      if ( hscroll && ( lt.x < 0 || br.x > 0 ) )
         scrollBy( lt.x < 0 ? lt.x : br.x, 0 );
   },

   setState: function( state, base, useAria ) {
      base = base || 'ved';

      switch ( state ) {
         case VED.TRISTATE_ON:
            this.addClass( base + '_on' );
            this.removeClass( base + '_off' );
            this.removeClass( base + '_disabled' );
            useAria && this.setAttribute( 'aria-pressed', true );
            useAria && this.removeAttribute( 'aria-disabled' );
            break;

         case VED.TRISTATE_DISABLED:
            this.addClass( base + '_disabled' );
            this.removeClass( base + '_off' );
            this.removeClass( base + '_on' );
            useAria && this.setAttribute( 'aria-disabled', true );
            useAria && this.removeAttribute( 'aria-pressed' );
            break;

         default:
            this.addClass( base + '_off' );
            this.removeClass( base + '_on' );
            this.removeClass( base + '_disabled' );
            useAria && this.removeAttribute( 'aria-pressed' );
            useAria && this.removeAttribute( 'aria-disabled' );
            break;
      }
   },

   getFrameDocument: function() {
      var $ = this.$;

      try {
         // In IE, with custom document.domain, it may happen that
         // the iframe is not yet available, resulting in "Access
         // Denied" for the following property access.
         $.contentWindow.document;
      } catch ( e ) {
         // Trick to solve this issue, forcing the iframe to get ready
         // by simply setting its "src" property.
         $.src = $.src;
      }

      return $ && new VED.dom.document( $.contentWindow.document );
   },

   copyAttributes: function( dest, skipAttributes ) {
      var attributes = this.$.attributes;
      skipAttributes = skipAttributes || {};

      for ( var n = 0; n < attributes.length; n++ ) {
         var attribute = attributes[ n ];

         // Lowercase attribute name hard rule is broken for
         // some attribute on IE, e.g. CHECKED.
         var attrName = attribute.nodeName.toLowerCase(),
            attrValue;

         // We can set the type only once, so do it with the proper value, not copying it.
         if ( attrName in skipAttributes )
            continue;

         if ( attrName == 'checked' && ( attrValue = this.getAttribute( attrName ) ) )
            dest.setAttribute( attrName, attrValue );
         // IE BUG: value attribute is never specified even if it exists.
         else if ( attribute.specified || ( VED.env.ie && attribute.nodeValue && attrName == 'value' ) ) {
            attrValue = this.getAttribute( attrName );
            if ( attrValue === null )
               attrValue = attribute.nodeValue;

            dest.setAttribute( attrName, attrValue );
         }
      }

      // The style:
      if ( this.$.style.cssText !== '' )
         dest.$.style.cssText = this.$.style.cssText;
   },

   renameNode: function( newTag ) {
      // If it's already correct exit here.
      if ( this.getName() == newTag )
         return;

      var doc = this.getDocument();

      // Create the new node.
      var newNode = new VED.dom.element( newTag, doc );

      // Copy all attributes.
      this.copyAttributes( newNode );

      // Move children to the new node.
      this.moveChildren( newNode );

      // Replace the node.
      this.getParent() && this.$.parentNode.replaceChild( newNode.$, this.$ );
      newNode.$[ 'data-ved-expando' ] = this.$[ 'data-ved-expando' ];
      this.$ = newNode.$;
   },

   getChild: (function() {
      function getChild( rawNode, index ) {
         var childNodes = rawNode.childNodes;

         if ( index >= 0 && index < childNodes.length )
            return childNodes[ index ];
      }

      return function( indices ) {
         var rawNode = this.$;

         if ( !indices.slice )
            rawNode = getChild( rawNode, indices );
         else {
            while ( indices.length > 0 && rawNode )
               rawNode = getChild( rawNode, indices.shift() );
         }

         return rawNode ? new VED.dom.node( rawNode ) : null;
      };
   })(),

   getChildCount: function() {
      return this.$.childNodes.length;
   },

   disableContextMenu: function() {
      this.on( 'contextmenu', function( event ) {
         // Cancel the browser context menu.
         if ( !event.data.getTarget().hasClass( 'ved_enable_context_menu' ) )
            event.data.preventDefault();
      });
   },

   getDirection: function( useComputed ) {
      if ( useComputed ) {
         return this.getComputedStyle( 'direction' ) ||
               this.getDirection() ||
               this.getParent() && this.getParent().getDirection( 1 ) ||
               this.getDocument().$.dir ||
               'ltr';
      }
      else
         return this.getStyle( 'direction' ) || this.getAttribute( 'dir' );
   },

   data: function( name, value ) {
      name = 'data-' + name;
      if ( value === undefined )
         return this.getAttribute( name );
      else if ( value === false )
         this.removeAttribute( name );
      else
         this.setAttribute( name, value );

      return null;
   },

   getEditor: function() {
      var instances = VED.instances,
         name, instance;

      for ( name in instances ) {
         instance = instances[ name ];

         if ( instance.element.equals( this ) && instance.elementMode != VED.ELEMENT_MODE_APPENDTO )
            return instance;
      }

      return null;
   }
});

   var sides = {
      width: [ 'border-left-width', 'border-right-width', 'padding-left', 'padding-right' ],
      height: [ 'border-top-width', 'border-bottom-width', 'padding-top', 'padding-bottom' ]
   };

   // Generate list of specific style rules, applicable to margin/padding/border.
   function expandedRules( style ) {
      var sides = [ 'top', 'left', 'right', 'bottom' ], components;

      if ( style == 'border' )
            components = [ 'color', 'style', 'width' ];

      var styles = [];
      for ( var i = 0 ; i < sides.length ; i++ ) {

         if ( components ) {
            for ( var j = 0 ; j < components.length ; j++ )
               styles.push( [ style, sides[ i ], components[j] ].join( '-' ) );
         }
         else
            styles.push( [ style, sides[ i ] ].join( '-' ) );
      }

      return styles;
   }

   function marginAndPaddingSize( type ) {
      var adjustment = 0;
      for ( var i = 0, len = sides[ type ].length; i < len; i++ )
         adjustment += parseInt( this.getComputedStyle( sides[ type ][ i ] ) || 0, 10 ) || 0;
      return adjustment;
   }

   VED.dom.element.prototype.setSize = function( type, size, isBorderBox ) {
      if ( typeof size == 'number' ) {
         if ( isBorderBox && !( VED.env.ie && VED.env.quirks ) )
            size -= marginAndPaddingSize.call( this, type );

         this.setStyle( type, size + 'px' );
      }
   };

   VED.dom.element.prototype.getSize = function( type, isBorderBox ) {
      var size = Math.max( this.$[ 'offset' + VED.tools.capitalize( type ) ], this.$[ 'client' + VED.tools.capitalize( type ) ] ) || 0;

      if ( isBorderBox )
         size -= marginAndPaddingSize.call( this, type );

      return size;
   };
})();





